import java.io.*;

/** @author Chris Howson & Steve Horsman */
abstract class Matrix
{
	/**Defines the number of rows in the Matrix */
	private int n_rows;
    /**Defines the number of columns in the Matrix */
	private int n_cols;
    /**Defines the type of the Matrix ('array' or 'list') */
    private String type;
    
    /**Returns the number of columns of the matrix*/
    public int getCols()
    {
		return n_cols;
	}

    /**Sets the number of columns of the matrix*/
	public void setCols(int n_cols)
	{
		this.n_cols = n_cols;
	}

	/**Gets the number of rows of the matrix*/
	public int getRows()
	{
		return n_rows;
	}

	/**Sets the number of rows of the matrix*/
	public void setRows(int n_rows)
	{
		this.n_rows = n_rows;
	}

	/**Sets the storage data type of the matrix*/
	public void setType(String type)
	{
		this.type = type;
	}

	/**Loads a txt file into a Matrix data type*/
    public Matrix load_Matrix(String filename)
    {
        BufferedReader in = null;
        String templine;
        String[] array;
        String type = "";
        Matrix newmatrix = null;
        int i = 0;
        try
        {
            //Create file readers
            FileReader f_reader= new FileReader(filename);
            in = new BufferedReader(f_reader);
            
            //Determine matrix type
            templine = in.readLine();
            if(templine.equals("Array")) { type = "Array"; }
            else if(templine.equals("List")) { type = "List"; }
            else { throw new Exception("Invalid Declaration of Matrix Type"); }

            //Set up array by getting sizes
            templine = in.readLine();
            array = templine.split(" ");
            if(type.equals("Array") && array.length == 2) { newmatrix = new MatrixArray(Integer.parseInt(array[0]), Integer.parseInt(array[1])); }
            else if(type.equals("List") && array.length == 2) { newmatrix = new MatrixList(Integer.parseInt(array[0]), Integer.parseInt(array[1])); }
            else { throw new Exception("Invalid Size Declaration"); }
            //While no more lines, read in new line and append to one string
            while((templine = in.readLine()) != null)
            {
                array = templine.split(" ");
                if(type.equals("List") && array.length == 3) { newmatrix.setCell(Integer.parseInt(array[0])-1, Integer.parseInt(array[1])-1, Double.parseDouble(array[2])); }
                if(type.equals("Array") && array.length == newmatrix.n_cols)
                {
                    for(int j = 0; j < array.length; j++)
                    {
                        newmatrix.setCell(i, j, Double.parseDouble(array[j]));
                    }
                    i++;
                }
            }
            //newmatrix.print();
        }
        catch (Exception e)
        {
            System.err.println("Error: " + e);
        }
        return newmatrix;
    }
    
    /** Returns whether one Matrix is identical to the another one*/
    public Boolean compare_matrices(Matrix b)
    {
    	if(this.n_rows != b.n_rows) { return false; }
    	if(this.n_cols != b.n_cols) { return false; }
    	for(int i = 0; i < this.n_rows; i++)
    	{
    		for(int j = 0; j < this.n_cols; j++)
    		{
    			if(this.getCell(i,j) != b.getCell(i,j)) { return false; }
    		}
    	}
    	return true;
    }
    
    /** Adds two matrices together*/
    public Matrix add_matrices(Matrix b)
    {
        Matrix t = null;
        //Add 2 matrices together
        if(this.type.equals("array") && b.type.equals("array") && this.n_rows == b.n_rows && this.n_cols == b.n_cols)
        {
            t = new MatrixArray(this.n_rows, this.n_cols);
        }
        else if(this.type.equals("list") && b.type.equals("list") && this.n_rows == b.n_rows && this.n_cols == b.n_cols)
        {
            t = new MatrixList(this.n_rows, this.n_cols);
        }
        for(int i = 0; i < this.n_rows; i++)
        {
            for(int j = 0; j < this.n_cols; j++)
            {
               t.setCell(i,j,(this.getCell(i,j)+b.getCell(i,j)));
            }
        }
        //t.print();
        return t;
    }
    
    /** Subtract two matrices*/
    public Matrix subtract_matrices(Matrix b)
    {
        Matrix t = null;
        //Add 2 matrices together
        if(this.type.equals("array") && b.type.equals("array") && this.n_rows == b.n_rows && this.n_cols == b.n_cols)
        {
            t = new MatrixArray(this.n_rows, this.n_cols);
        }
        else if(this.type.equals("list") && b.type.equals("list") && this.n_rows == b.n_rows && this.n_cols == b.n_cols)
        {
            t = new MatrixList(this.n_rows, this.n_cols);
        }
        for(int i = 0; i < this.n_rows; i++)
        {
            for(int j = 0; j < this.n_cols; j++)
            {
               t.setCell(i,j,(this.getCell(i,j)-b.getCell(i,j)));
            }
        }
        //t.print();
        return t;
    }

    /**Multiplies two matrices together and returns the result*/
    public Matrix multiply(Matrix b)
    {
    	//check for compatiablilty
        if(!(this.n_cols == b.n_rows))
        {
        	System.err.println("Incompatible types: " + this.n_rows + "," + this.n_cols +" ; "+ b.n_rows + "," + b.n_cols);
            System.exit(1);
        }
        int rowno = this.n_rows;
        int colno = b.n_cols;
        int midno = b.n_rows;
    	

    	Matrix c = null;
        if(this.type == "array")
        {
            c = new MatrixArray(b.n_cols, this.n_rows);
        }
        else if(this.type == "list")
        {
            c = new MatrixList(b.n_cols, this.n_rows);
        }

        for (int i=0;i<rowno;i++)
        {
            for (int k=0;k<midno;k++)
            {
                for(int j = 0;j<colno;j++)
                {
                	double cell = c.getCell(i,j) + (this.getCell(i,k)*b.getCell(k,j));
                    c.setCell(i,j,cell);
                }
            }
        }
        return c;
    }

    /**Returns the Transpose of a given matrix */
    public Matrix transpose()
    {
        Matrix t = null;
        if(this.type.equals("array"))
        {
            t = new MatrixArray(this.n_cols, this.n_rows);
        }
        else if(this.type.equals("list"))
        {
            t = new MatrixList(this.n_cols, this.n_rows);
        }
        for(int i = 0; i < this.n_rows; i++)
        {
            for(int j = 0; j < this.n_cols; j++)
            {
                t.setCell(j, i, getCell(i, j));
            }
        }
        return t;
    }

    /**Returns the determinant of a given Matrix*/
    public double determinant()
    {
    	double result;
        if (this.n_rows != this.n_cols)
        {
            System.err.println("Matrix must be square to have a determinant");
            result = 0;
        }
        else if (this.n_rows == 2)
        {
        	double ad = this.getCell(0,0)*this.getCell(1,1);
        	double bc = this.getCell(1,0)*this.getCell(0,1);
            result = ad - bc;
        }
        else
        {
        	double sum = 0;
            for(int i = 0;i<this.n_rows;i++)
            {
                double detpart = this.getCell(0,i)*cofactor(this,0,i);
                sum = sum + detpart;
            }
            result = sum;
        }
        return result;
    }
    
    /**Calculates the cofactor of a matrix*/
    double cofactor(Matrix a, int row, int col)
    {
    	double cofactor;
    	if((row+col)%2 == 0)
        {
        	cofactor = 1;
        	
        }
        else
        {
        	cofactor = -1;
        }
    	Matrix M0i = minor(a, row,col);
    	double ans = cofactor * M0i.determinant();
    	return ans;
    }

    /**Calculates the minor of a matrix*/
    Matrix minor(Matrix a, int row, int col)
    {
        int n1 = a.n_rows-1;
        Matrix newMat = null;
        if(a.type.equals("array"))
        {
        	newMat = new MatrixArray(n1,n1);
        }
        else if(a.type.equals("list"))
        {
        	newMat = new MatrixList(n1,n1);
        }
        int k=0;
        int l=0;
        for(int i =0;i<=n1;i++)
        {
            k=0;
            if(i!=row)
            {
                for(int j = 0;j<=n1;j++)
                {
                    if(j!=col)
                    {
                        double ij = a.getCell(i,j);
                        if(ij != 0)
                        {
                            newMat.setCell(l,k,ij);
                        }
                        k++;
                    }
                }
                l++;
            }
        }
        return newMat;
    }
    
    
    /**Returns the inverse of a Matrix*/
    public Matrix inverse()
    {
    	Matrix inv = null;
        if(this.type.equals("array"))
        {
        	inv = new MatrixArray(this.n_rows, this.n_cols);
        }
        else if(this.type.equals("list"))
        {
        	inv = new MatrixList(this.n_rows, this.n_cols);
        }
        
    	double det = this.determinant();
    	if(det == 0)
    	{
    		System.err.println("This matrix has no inverse");
            return null;
    	}
    	for (int i = 0;i<this.n_rows;i++)
    	{
    		for (int j = 0;j<this.n_cols;j++)
    		{
    			double cell = (1/det)*cofactor(this,j,i);
    		    inv.setCell(i,j,cell);
    		}
    	}
    	return inv;
    }
    
    /**Multiplies each element by a scalar in a given Matrix*/
    public Matrix scalarmul(double scalar)
    {
    	Matrix t = null;
        if(this.type.equals("array"))
        {
            t = new MatrixArray(this.getRows(), this.getCols());
        }
        else if(this.type.equals("list"))
        {
            t = new MatrixList(this.getRows(), this.getCols());
        }
        for(int i = 0; i < this.getRows(); i++)
        {
            for(int j = 0; j < this.getCols(); j++)
            {
                t.setCell(i,j, (scalar*this.getCell(i,j)));
            }
        }
        return t;
    }
    
    /**Prints the contents of a matrix to Standard Output*/
    void print()
    {
        for(int i = 0; i < this.getRows(); i++)
        {
            for(int j = 0; j < this.getCols(); j++)
            {
                System.out.print("" + getCell(i,j) + " ");
            }
            System.out.println();
        }
    }

    /**Returns the data stored in the specifed cell*/
    abstract double getCell(int row, int col);
    /**Sets the data stored in the specifed cell to the given double*/
    abstract void setCell(int row, int col, double data);
    /**Deletes the entry in cell (row,col)*/
    abstract void deleteCell(int row, int col);
    /**Returns the type for a given Matrix*/
    abstract String getType();
}
